package cn.edu.dutir.searchengine;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import cn.edu.dutir.utility.ObjectToMap;
import cn.edu.dutir.utility.ObjectToSet;
import cn.edu.dutir.utility.Scored;
import cn.edu.dutir.utility.ScoredObject;
import cn.edu.dutir.utility.SparseVector;
import cn.edu.dutir.utility.SymbolTable;

/**
 * 
 * The IndexSearcher class is a wrapper, which provide search service. The
 * functionality is as follows<br>
 * <ul>
 * <li> Load the index(both the inverted and forward index) into main memory. </li>
 * <li> Analyze user posed query and transform it into an acceptable form for the index.</li>
 * <li> Invoke the retrieval process and get the relevance document set</li>
 * <li> To each relevance document, calculate the similarity score with regard to the query vector </li>
 * <li> Sort the relevance document according to the relevance score in descending order and return. </li>
 * </ul>
 */
public class IndexSearcher {

	private static File mIndexDir = null;
	/**
	 * The number of search result displayed, using 50 as default.
	 */
	private static int mHitNum = 50;

	/**
	 * docid -> forward term list
	 * 
	 */
	private Map<Integer, SparseVector> mFwddIdx = null;
	/**
	 * Maps from indexed terms to its unique identifies(ids) and vice versa.<br>
	 * <big><i>Note</i></big>:In both the inverted and forward index, terms and document are
	 * represented as unique integer values. By doing this, the index size can
	 * be reduced , thus saving much storage space which is of much importance in
	 * the construction and maintenance of index.
	 * 
	 */
	private SymbolTable mIdxSymbolTable = null;

	/**
	 * Maps from document name to its unique identifies(ids) and vice versa.
	 */
	private SymbolTable mDocSymbolTable = null;
	/**
	 * term -> &lt;docid , docid, ... &gt;<br>
	 * Maps from (term id) to the (document set) that contains the corresponding term. 
	 * It is used as a inverted index.
	 */
	private ObjectToSet<Integer, Integer> mIvtIdx = null;
	/**
	 * Here, dimension refers to the size of the index, namely the unique term
	 * in the inverted index. So, from the numerical point of view,
	 * <code>mNumDimension</code> equals to
	 * <code> mIvtIdx.keySet().size</code> The aim of keeping this variable
	 * here ,is to improve the speed of query analysis, and can be seen as a
	 * caching mechanism
	 */
	private int mNumDimension = -1;

	private IndexSearcher() {
	}

	/**
	 *  Construct a IndexSearcher by the specified path.
	 *  
	 * @param indexPath The path of the index.
	 * @throws FileNotFoundException
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public IndexSearcher(String indexPath) throws FileNotFoundException,
			IOException, ClassNotFoundException {
		this(new File(indexPath));
	}

	/**
	 * Construct a IndexSearcher by the specified directory where index stored
	 * in.
	 * 
	 * @param indexDir The directory where index stores. 
	 * @throws FileNotFoundException
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public IndexSearcher(File indexDir) throws FileNotFoundException,
			IOException, ClassNotFoundException {
		mIndexDir = indexDir;
		File objFile = new File(indexDir, "objIdx");
		ObjectInput in = new ObjectInputStream(new FileInputStream(objFile));
		IndexSearcher indexSearcher = (IndexSearcher) new Externalizer()
				.read(in);
		this.mNumDimension = indexSearcher.mNumDimension;
		this.mIdxSymbolTable = indexSearcher.mIdxSymbolTable;
		this.mDocSymbolTable = indexSearcher.mDocSymbolTable;
		this.mFwddIdx = indexSearcher.mFwddIdx;
		this.mIvtIdx = indexSearcher.mIvtIdx;
	}

	/**
	 * The core procedure of the IndexSeacher class. <br>
	 * <ul>
	 * <li>Given a query, firstly extract each possible query term and construct a
	 * query vector(in our implementation, it is a {@link SparseVector}. <br></li>
	 * <li>Then search for the inverted index and obtain the relevance document set.
	 * For each possible relevance document, calculate the relevance(similarity)
	 * score with respect to the query</li>
	 * <li>Finally, sort the relevance document by relevance score in descending order
	 * and return</li>
	 * </ul>
	 * @param queryWords The user posed query.
	 * @return
	 */
	public ScoredObject<Integer>[] search(String queryWords[]) {
		if (queryWords == null || queryWords.length < 1) {
			return new ScoredObject[0];
		}
		/**
		 * extract each possible query term and construct a query vector.
		 */
		SparseVector queryVector = getQueryVector(queryWords);
		Set<Integer> docIdSet = new HashSet<Integer>();
		/**
		 * obtain the relevance document set according to the inverted index.
		 */
		for (String query : queryWords) {
			docIdSet.addAll(mIvtIdx.getSet(mIdxSymbolTable
					.symbolToIDInteger(query.toLowerCase())));
		}
		List<ScoredObject<Integer>> retList = new ArrayList<ScoredObject<Integer>>(
				docIdSet.size());
		Iterator<Integer> iter = docIdSet.iterator();
		while (iter.hasNext()) {
			Integer docid = iter.next();
			SparseVector docVector = mFwddIdx.get(docid);
			/**
			 * calculate the similarity score between each document and the query, in our
			 * implementation, cosine measure are adopted.
			 */
			double score = queryVector.cosine(docVector);
			retList.add(new ScoredObject<Integer>(docid, score));
		}
		/**
		 * Sort the relevence document set according to similarity score in descending order.
		 */
		Collections.sort(retList, Scored.REVERSE_SCORED_COMPARATOR);
		ScoredObject<Integer> hits[] = new ScoredObject[retList.size()];
		retList.toArray(hits);
		return hits;
	}

	/**
	 * Construct the query vector. Each query word contained in inverted index
	 * constitute one dimension. In our implementation, binary weight
	 * coefficient are adopted, namely setting to if contains corresponding
	 * query word appear in the inverted index, otherwise 0.
	 * 
	 * @param queryWords The user posed query.
	 * @return
	 */
	public SparseVector getQueryVector(String queryWords[]) {
		int keys[] = new int[queryWords.length];
		double values[] = new double[queryWords.length];
		int idx = 0;
		
		for (String query : queryWords) {
			int key = mIdxSymbolTable.symbolToID(query.toLowerCase());
			query.indexOf("");
			if (idx != -1) {
				keys[idx] = key;
				values[idx++] = 1.0;
			} else {// reserve zero dimension
				keys[idx] = 0;
				values[idx++] = 0.0;
			}
		}
		Arrays.sort(keys);
		return new SparseVector(keys, values, mNumDimension);
	}

	/**
	 * Return a readable version of the returned relevant document set, which is
	 * just a string-based representation of the result.
	 * 
	 * @param hits The relevant document set
	 * @return
	 */
	public static String hitsToString(ScoredObject<Integer>[] hits) {
		StringBuffer sb = new StringBuffer();
		sb.append("rank\tdocid\t\tscore\r\n");
		for (int i = 0, n = hits.length; i < n; i++) {
			sb.append((i + 1) + "\t" + hits[i].getObject() + "\t"
					+ hits[i].score() + "\r\n");
		}
		return sb.toString();
	}


	/**
	 * Return a readable version of the returned relevant document set, which is
	 * just a string-based representation of the result Compared to
	 * {@link hitToString(ScoredObject<Integer>[] hits)} method, this procedure
	 * maps document identifies to its name to make the representation more
	 * readable.
	 * 
	 * @param hits The relevant document set
	 * @param docSymbolTable The symbol table that maps the document ids to its name.
	 * @return
	 */
	public static String hitsToString(ScoredObject<Integer>[] hits,
			SymbolTable docSymbolTable) {
		StringBuffer sb = new StringBuffer();
		if (hits.length == 0) {
			sb.append("no documents matched");
			return sb.toString();
		}
		sb.append("rank\t\tdocid\t\tscore\r\n");
		for (int i = 0, n = Math.min(mHitNum, hits.length); i < n; i++) {
			sb.append((i + 1) + "\t"
					+ docSymbolTable.idToSymbol(hits[i].getObject()) + "\t"
					+ hits[i].score() + "\r\n");
		}
		return sb.toString();
	}
	
	/**
	 * Return the the document name set.
	 * 
	 * @param hits The relevant document set
	 * @param docSymbolTable The symbol table that maps the document ids to its name.
	 * @return
	 */
	public static Set<String> hitsToDocSet(ScoredObject<Integer>[] hits,
			SymbolTable docSymbolTable) {
		Set<String> docSet = new HashSet<String>(hits != null ? hits.length : 0);
		for (int i = 0, n = hits.length; i < n; i++) {
			docSet.add(docSymbolTable.idToSymbol(hits[i].getObject()));
		}
		return docSet;
	}

	/**
	 * Convert a collection of objects to a string-based representation, 
	 * each object a line.
	 * 
	 * @param c The collection to convert
	 * @return
	 */
	public static String collectionToString(Collection<?> c) {
		StringBuffer sb = new StringBuffer();
		for (Object o : c) {
			sb.append(o.toString() + "\r\n");
		}
		return sb.toString();
	}

	/**
	 * Writer a string value to a Output file
	 *  
	 * @param outFile The file to write 
	 * @param value The string to be written.
	 * @throws IOException
	 */
	public static void writeString(File outFile, String value)
			throws IOException {
		BufferedWriter writer = null;
		FileWriter fw = null;
		try {
			fw = new FileWriter(outFile);
			writer = new BufferedWriter(fw);
			writer.write(value);
		} finally {
			writer.close();
			fw.close();
		}
	}
	/**
	 * A Externalizer is a serialization class used for outputting or read in a
	 * instance of the IndexSearcher object.
	 * 
	 */
	private static class Externalizer {
		private static final long serialVersionUID = -6040616216389802649L;
		final IndexSearcher mIndexSearcher;

		public Externalizer() {
			mIndexSearcher = null;
		}

		public Externalizer(IndexSearcher indexSearcher) {
			mIndexSearcher = indexSearcher;
		}

		/**
		 * Read in a IndexSearcher object from the specified object input stream.
		 * 
		 * @param in The specified object input stream.
		 * @return A instance of IndexSearcher object.
		 * @throws IOException
		 * @throws ClassNotFoundException
		 */
		public Object read(ObjectInput in) throws IOException,
				ClassNotFoundException {
			IndexSearcher indexSearcher = new IndexSearcher();
			indexSearcher.mNumDimension = in.readInt();
			indexSearcher.mIdxSymbolTable = SymbolTable.readObject(in);
			indexSearcher.mDocSymbolTable = SymbolTable.readObject(in);
			indexSearcher.mFwddIdx = (Map<Integer, SparseVector>) in
					.readObject();
			indexSearcher.mIvtIdx = (ObjectToSet<Integer, Integer>) in
					.readObject();
			return indexSearcher;
		}

		/**
		 * Serialize current IndexSearcher object into the specified object
		 * output stream.
		 * 
		 * @param objOut The specified object output stream.
		 * @throws IOException
		 */
		public void writeExternal(ObjectOutput objOut) throws IOException {
			// objOut.writeInt(mSymbolTable.mSymbolToId.size());
			objOut.writeInt(mIndexSearcher.mNumDimension);
			mIndexSearcher.mIdxSymbolTable.writeReplace(objOut);
			mIndexSearcher.mDocSymbolTable.writeReplace(objOut);
			objOut.writeObject(mIndexSearcher.mFwddIdx);
			objOut.writeObject(mIndexSearcher.mIvtIdx);
		}
	}

	/**
	 * Parse environment argument passed in from user.
	 * 
	 * @param args Environment argument passed in.
	 */
	public static void parserArgv(String args[]) {
		if (args == null || args.length < 2) {
			usage();
		}
		for (int i = 0; i < args.length; i++) {
			if (args[i].equals("-i")) {
				mIndexDir = new File(args[++i]);
			} else if (args[i].equals("-n")) {
				mHitNum = Integer.parseInt(args[++i]);
			} else {
				usage();
			}
		}
		if (mIndexDir == null) {
			String msg = "indexPath must be specified" + mIndexDir;
			throw new IllegalArgumentException(msg);
		}
		if (!mIndexDir.exists() || !mIndexDir.isDirectory()) {
			String msg = "indexPath must exist and be a directory, but found indexDir ="
					+ mIndexDir;
			throw new IllegalArgumentException(msg);
		}
	}

	/**
	 * Display usage message.
	 */
	public static void usage() {
		String msg = "Usage:\t IndexSearcher [-i indexPath] {-n hitNum}\r\n"
				+ "\t -i indexPath ---- indicate the path of index\r\n"
				+ "\t -n hitNum    ---- set up the number of hit to displace, using 50 as default\r\n"
				+ "\t enter 'quit' or 'exit' to exit";
		System.out.println(msg);
		System.exit(1);
	}

	/**
	 * The main process.
	 * @param args 
	 */
	public static void main(String args[]) throws FileNotFoundException,
			IOException, ClassNotFoundException {
		parserArgv(args);
		IndexSearcher searcher = new IndexSearcher(mIndexDir);
		BufferedReader reader = null;
		reader = new BufferedReader(new InputStreamReader(System.in));
		String query = null;
		while (true) {
			System.out.print("Query:\t");
			query = reader.readLine();
			if (query == "" || query.trim().length() == 0) {
				continue;
			}
			if (query.equalsIgnoreCase("quit")
					|| query.equalsIgnoreCase("exit")) {
				break;
			}
			String queryWords[] = query.split(" ");
			ScoredObject<Integer> hits[] = searcher.search(queryWords);
			System.out.println(hitsToString(hits, searcher.mDocSymbolTable));
//			writeString(new File("index/answer_2.txt"),
//					collectionToString(hitsToDocSet(hits,
//							searcher.mDocSymbolTable)));
		}
		reader.close();
		System.out.println("[logout]");
	}
}
